package me.luger.core.mvc.filter.pagecache;


import me.luger.core.commom.util.EhCacheUtil;
import me.luger.core.commom.util.LogUtil;
import me.luger.core.commom.util.SpelUtil;
import me.luger.core.commom.util.StringUtil;
import me.luger.core.mvc.WebService;
import me.luger.core.mvc.log.access.AccessLogEntity;
import me.luger.core.mvc.filter.pagecache.annotation.PageCache;
import me.luger.core.mvc.filter.pagecache.entity.WebPageInfo;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.constructs.blocking.BlockingCache;
import net.sf.ehcache.constructs.blocking.LockTimeoutException;
import net.sf.ehcache.constructs.web.*;
import net.sf.ehcache.constructs.web.filter.CachingFilter;
import net.sf.ehcache.constructs.web.filter.Filter;
import net.sf.ehcache.constructs.web.filter.FilterNonReentrantException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerMapping;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.util.*;
import java.util.zip.DataFormatException;
import java.util.zip.GZIPOutputStream;

/**
 * @author TianHaoyuan
 * @Title WebPageCacheFilter.java
 * @Description 页面缓存过滤器，可以对不同url配置不同的ehcache
 * @Copyright 2016 © kuwo.cn
 * @Created 2016/01/14
 * @Version: 1.0
 */
//TODO 后面看看能不能改到拦截器中不用过滤器
public class PageCacheFilter extends Filter {

    private static final String UNDERLINE_MARK = "_";

    private static final Logger LOG = LoggerFactory.getLogger(CachingFilter.class);

    private static final String CALLBACK_NAME = "callback";

    private final VisitLog visitLog = new VisitLog();

    public void doInit(FilterConfig filterConfig) throws CacheException {
        synchronized (this.getClass()) {
            //TODO
        }
    }

    protected void doDestroy() {
    }

    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws AlreadyGzippedException, AlreadyCommittedException, FilterNonReentrantException, LockTimeoutException, Exception {
        boolean breakBuildPageInfo = true;
        try {
            String uri = request.getRequestURI();
            List<String> allRequestUrls = WebService.getRequestUrlList();

            if (!allRequestUrls.contains(uri)) {
                return;
            }
//            if(!uri.startsWith("/api")){
//                return;
//            }
            Map<String, PageCache> cacheConfigMapping = WebService.getPageCacheUrlMapping();
            PageCache pageCache = cacheConfigMapping.get(uri);
            if (null == pageCache) {
                return;
            }
            Object obj = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            Map<String,String> pathVariablesMap = (Map<String, String>) obj;

            String keyPrefix = pageCache.keyPrefix();
            int timeToLiveSeconds = pageCache.timeToLiveSeconds();
            String[] keyParams = pageCache.keyParams();
            String key = calculateKey(request, keyPrefix,pathVariablesMap,keyParams);
            if (response.isCommitted()) {
                AlreadyCommittedException exception = new AlreadyCommittedException(
                        "Response already committed before doing buildPage.");
                LogUtil.errorLog(exception, null);
                throw exception;
            }
            logRequestHeaders(request);
            breakBuildPageInfo = false;

            WebPageInfo webPageInfo = buildPageInfo(request, response, chain, key,timeToLiveSeconds);

            if (webPageInfo.isOk()) {
                if (response.isCommitted()) {
                    AlreadyCommittedException exception = new AlreadyCommittedException(
                            "Response already committed after doing buildPage"
                                    + " but before writing response from PageInfo.");
                    LogUtil.errorLog(exception, null);
                    throw exception;
                }
                writeResponse(request, response, webPageInfo);
            }
        } finally {
            if (breakBuildPageInfo) {
                chain.doFilter(request, response);
            }
        }

    }

    protected WebPageInfo buildPageInfo(HttpServletRequest request, HttpServletResponse response, FilterChain chain, String cacheKey,int timeToLiveSeconds) throws Exception {
        String key = cacheKey;
        WebPageInfo webPageInfo = null;
        try {
            checkNoReentry(request);
            Object cacheValue = EhCacheUtil.get(key);
            if (cacheValue != null ) { //代表有缓存
                AccessLogEntity logEntity = WebService.getAccessLogEntity();
                if (null != logEntity) {
                    logEntity.setUseCache(true);
                }
                webPageInfo = (WebPageInfo) cacheValue;
                webPageInfo.setCache(WebPageInfo.CACHE_USE);
            } else {
                try {
                    webPageInfo = this.buildPage(request, response, chain, timeToLiveSeconds);
                    if (webPageInfo.isOk()) {
                        EhCacheUtil.put(key,webPageInfo,timeToLiveSeconds);
                    }
                } catch (Throwable var12) {
                    throw new Exception(var12);
                }
            }
        } catch (LockTimeoutException var13) {
            throw var13;
        } finally {
            this.visitLog.clear();
        }

        return webPageInfo;
    }

    protected WebPageInfo buildPage(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                    int timeToLiveSeconds) throws AlreadyGzippedException, Exception {
        ByteArrayOutputStream outstr = new ByteArrayOutputStream();
        GenericResponseWrapper wrapper = new GenericResponseWrapper(response, outstr);
        chain.doFilter(request, wrapper);
        wrapper.flush();
        return new WebPageInfo(wrapper.getStatus(), wrapper.getContentType(), wrapper.getCookies(), outstr.toByteArray(), true, timeToLiveSeconds, wrapper.getAllHeaders());
    }

    protected void writeResponse(HttpServletRequest request, HttpServletResponse response, PageInfo pageInfo) throws IOException, DataFormatException, ResponseHeadersNotModifiableException {
        boolean requestAcceptsGzipEncoding = this.acceptsGzipEncoding(request);
        this.setStatus(response, pageInfo);
        this.setContentType(response, pageInfo);
        this.setCookies(pageInfo, response);
        this.setHeaders(pageInfo, requestAcceptsGzipEncoding, response);
        this.writeContent(request, response, pageInfo);
    }

    protected void setContentType(HttpServletResponse response, PageInfo pageInfo) {
        String contentType = pageInfo.getContentType();
        if (contentType != null && contentType.length() > 0) {
            response.setContentType(contentType);
        }

    }

    protected void setCookies(PageInfo pageInfo, HttpServletResponse response) {
        List cookies = pageInfo.getSerializableCookies();
        Iterator iterator = cookies.iterator();

        while (iterator.hasNext()) {
            Cookie cookie = ((SerializableCookie) iterator.next()).toCookie();
            response.addCookie(cookie);
        }

    }

    protected void setStatus(HttpServletResponse response, PageInfo pageInfo) {
        response.setStatus(pageInfo.getStatusCode());
    }

    protected void setHeaders(final PageInfo pageInfo,
                              boolean requestAcceptsGzipEncoding,
                              final HttpServletResponse response) {

        final Collection<Header<? extends Serializable>> headers = pageInfo
                .getHeaders();

        final TreeSet<String> setHeaders = new TreeSet<String>(
                String.CASE_INSENSITIVE_ORDER);

        for (final Header<? extends Serializable> header : headers) {
            final String name = header.getName();

            switch (header.getType()) {
                case STRING:
                    if (setHeaders.contains(name)) {
                        response.addHeader(name, (String) header.getValue());
                    } else {
                        setHeaders.add(name);
                        response.setHeader(name, (String) header.getValue());
                    }
                    break;
                case DATE:
                    if (setHeaders.contains(name)) {
                        response.addDateHeader(name, (Long) header.getValue());
                    } else {
                        setHeaders.add(name);
                        response.setDateHeader(name, (Long) header.getValue());
                    }
                    break;
                case INT:
                    if (setHeaders.contains(name)) {
                        response.addIntHeader(name, (Integer) header.getValue());
                    } else {
                        setHeaders.add(name);
                        response.setIntHeader(name, (Integer) header.getValue());
                    }
                    break;
                default:
                    throw new IllegalArgumentException("No mapping for Header: "
                            + header);
            }
        }
    }


    protected String calculateKey(HttpServletRequest request, String keyPrefix,Map<String,String> otherParaMap, String[] keyParams) {
        if(StringUtil.isBlank(keyPrefix)){
            keyPrefix = request.getRequestURI().replaceAll("/",UNDERLINE_MARK);
        }
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(keyPrefix).append(UNDERLINE_MARK);
        if (null != keyParams && keyParams.length > 0) {
            String value = null;
            for (String param : keyParams) {
                value = request.getParameter(param);
                if(null == value){
                    value = otherParaMap.get(param);
                }
                stringBuffer.append(value).append(UNDERLINE_MARK);
            }
        }
//        else {
//            stringBuffer.append(UNDERLINE_MARK).append(
//                    request.getQueryString());
//        }
        String rformatVal = request.getParameter("rformat");
        if(null != rformatVal && ("json".equals(rformatVal) || "xml".equals(rformatVal))){
            stringBuffer.append(rformatVal).append(UNDERLINE_MARK);
        }
        String key = stringBuffer.toString();
        return key;
    }

    protected void writeContent(HttpServletRequest request, HttpServletResponse response, PageInfo pageInfo) throws IOException, ResponseHeadersNotModifiableException {
        boolean shouldBodyBeZero = ResponseUtil.shouldBodyBeZero(request, pageInfo.getStatusCode());
        byte[] body;
        String callbackValue = request.getParameter(CALLBACK_NAME);

        if (StringUtils.isNotBlank(callbackValue)) {
            body = getJsonpBody(request, response, pageInfo, callbackValue);
        } else {
            body = getBody(request, response, pageInfo);
        }

        response.setContentLength(body.length);
        BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
        out.write(body);
        out.flush();
    }

    protected void checkNoReentry(HttpServletRequest httpRequest) throws FilterNonReentrantException {
        String filterName = this.getClass().getName();
        if (this.visitLog.hasVisited()) {
            throw new FilterNonReentrantException("The request thread is attempting to reenter filter " + filterName + ". URL: " + httpRequest.getRequestURL());
        } else {
            this.visitLog.markAsVisited();
            if (LOG.isDebugEnabled()) {
                LOG.debug("Thread {}  has been marked as visited.", Thread.currentThread().getName());
            }

        }
    }

    private byte[] getBody(HttpServletRequest request,
                           HttpServletResponse response, PageInfo pageInfo) throws IOException {
        byte[] body = null;
        boolean shouldBodyBeZero = ResponseUtil.shouldBodyBeZero(request,
                pageInfo.getStatusCode());

        if (shouldBodyBeZero) {
            body = new byte[0];
        } else if (acceptsGzipEncoding(request)) {
            body = pageInfo.getGzippedBody();
            if (ResponseUtil.shouldGzippedBodyBeZero(body, request)) {
                body = new byte[0];
            } else {
                ResponseUtil.addGzipHeader(response);
            }
        } else {
            body = pageInfo.getUngzippedBody();
        }
        return body;
    }

    private byte[] getJsonpBody(HttpServletRequest request,
                                HttpServletResponse response, PageInfo pageInfo,
                                String jpCallbackValue) throws IOException {
        byte[] body = null;
        boolean shouldBodyBeZero = ResponseUtil.shouldBodyBeZero(request,
                pageInfo.getStatusCode());

        if (shouldBodyBeZero) {
            body = new byte[0];
        } else if (acceptsGzipEncoding(request)) {
            body = pageInfo.getGzippedBody();
            if (ResponseUtil.shouldGzippedBodyBeZero(body, request)) {
                body = new byte[0];
            } else {
                ResponseUtil.addGzipHeader(response);
                byte[] unzipBody = pageInfo.getUngzippedBody();
                String bodyJson = new String(unzipBody, "utf-8");
                if (StringUtils.isNotBlank(bodyJson)) {
                    if (bodyJson.contains("({") && bodyJson.contains("})")) {
                        bodyJson = bodyJson.substring(bodyJson.indexOf("(") + 1, bodyJson.lastIndexOf(")"));
                    }
                    bodyJson = String.format("%s(%s)", jpCallbackValue,
                            bodyJson);
                    body = gzipContent(bodyJson);
                }
            }
        } else {
            byte[] unzipBody = pageInfo.getUngzippedBody();
            String bodyJson = new String(unzipBody, "utf-8");
            if (StringUtils.isNotBlank(bodyJson)) {
                if (bodyJson.contains("({") && bodyJson.contains("})")) {
                    bodyJson = bodyJson.substring(bodyJson.indexOf("(") + 1, bodyJson.lastIndexOf(")"));
                }
                bodyJson = String.format("%s(%s)", jpCallbackValue, bodyJson);
                body = bodyJson.getBytes();
            } else {
                body = pageInfo.getUngzippedBody();
            }

        }
        return body;
    }

    private byte[] gzipContent(String body) throws IOException,
            AlreadyGzippedException {
        final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(bytes);
        gzipOutputStream.write(body.getBytes());
        gzipOutputStream.close();
        return bytes.toByteArray();
    }

    private static class VisitLog extends ThreadLocal<Boolean> {
        private VisitLog() {
        }

        protected Boolean initialValue() {
            return Boolean.valueOf(false);
        }

        public boolean hasVisited() {
            return ((Boolean) this.get()).booleanValue();
        }

        public void markAsVisited() {
            this.set(Boolean.valueOf(true));
        }

        public void clear() {
            super.remove();
        }
    }
}

